/**
 * 
 */
package tau.adx.simulator;

import java.util.Collection;

import lombok.Data;

import org.apache.log4j.Logger;

import tau.adx.common.AdxConstants;
import tau.adx.common.adn.AdNetwork;
import tau.adx.common.bids.BidInfo;
import tau.adx.common.bids.bundles.AdxBidBundle;
import tau.adx.common.devices.Device;
import tau.adx.common.enteties.ranking.RankingAgency;
import tau.adx.common.messages.auction.AuctionCanceled;
import tau.adx.common.messages.auction.AuctionWon;
import tau.adx.common.messages.auction.CancellationReason;
import tau.adx.common.publishers.AdxPublisher;
import tau.adx.common.users.AdxUser;
import tau.adx.common.users.properties.Age;
import tau.adx.common.users.properties.Gender;
import tau.adx.common.users.properties.Income;

import com.google.common.eventbus.EventBus;
import com.google.inject.Inject;

/**
 * @author greenwald
 * 
 */
@Data
public class AdX {

	/**
	 * The system's main event bus.
	 */
	public static EventBus eventBus = new EventBus(
			AdxConstants.ADX_EVENT_BUS_NAME);

	/**
	 * The AdX singleton instance.
	 */
	private static AdX instance;

	/**
	 * {@link Logger} instance.
	 */
	private static final Logger logger = Logger.getLogger(AdX.class);

	/**
	 * Calculates the popularity of a {@link Device device} among all others.
	 * 
	 * @param device
	 *            An {@link Device}.
	 * @return The {@link Device device}'s popularity among all other.
	 */
	public static double getDevicePopularity(Device device) {
		return 1 / (Device.values().length);
	}

	/**
	 * @return The AdX singleton instance.
	 */
	public static AdX getInstance() {
		return instance;
	}

	/**
	 * Calculates the popularity of a {@link AdxUser user} among all others.
	 * 
	 * @param user
	 *            An {@link AdxUser}.
	 * @return The {@link AdxUser user}'s popularity among all other.
	 */
	public static double getUserPopularity(AdxUser user) {
		return 1 / (Age.values().length * Gender.values().length * Income
				.values().length);
	}

	/**
	 * Initializes the AdX singleton.
	 * 
	 * @param adX
	 *            An instance of the {@link AdX}.
	 */
	public static void init(AdX adX) {
		if (instance != null) {
			logger.error("AdX has been initialized more than once");
		}
		instance = adX;
	}

	/**
	 * Registered {@link AdNetwork ad networks} in the system.
	 */
	private final Collection<AdNetwork> adNetworks;

	/**
	 * Registered {@link AdxBidBundle bid bundles} in the system.
	 */
	private Collection<AdxBidBundle> bidBundles;

	/**
	 * Indicates whether minor attributes, such as {@link AdxUser}'s pContinue,
	 * should be set to 0 or null.
	 */
	private boolean ignoreMinorAttributes;

	/**
	 * Registered {@link AdxPublisher publishers} in the system.
	 */
	private final Collection<AdxPublisher> publishers;

	/**
	 * Registered {@link RankingAgency ranking agency} in the system.
	 */
	private final RankingAgency rankingAgency;

	/**
	 * Registered {@link AdxUser users} in the system.
	 */
	private final Collection<AdxUser> users;

	/**
	 * @param users
	 *            Registered {@link AdxUser users} in the system.
	 * @param publishers
	 *            Registered {@link AdxPublisher publishers} in the system.
	 * @param bidBundles
	 *            Registered {@link AdxBidBundle bid bundles} in the system.
	 * @param rankingAgency
	 *            Registered {@link RankingAgency ranking agency} in the system.
	 * @param adNetworks
	 *            Registered {@link AdNetwork ad networks} in the system.
	 */
	@Inject
	public AdX(Collection<AdxUser> users, Collection<AdxPublisher> publishers,
			Collection<AdxBidBundle> bidBundles, RankingAgency rankingAgency,
			Collection<AdNetwork> adNetworks) {
		this.users = users;
		this.publishers = publishers;
		this.bidBundles = bidBundles;
		this.rankingAgency = rankingAgency;
		this.adNetworks = adNetworks;
		// TODO find a better way to inject value with Guice
		this.ignoreMinorAttributes = true;
	}

	/**
	 * Runs an auction for a given {@link AdxUser user} and {@link AdxPublisher
	 * publisher}.
	 * 
	 * @param user
	 *            {@link AdxUser} to run auction for.
	 * @param publisher
	 *            {@link AdxPublisher} to run auction for.
	 * @param reservePrice
	 *            {@link AdxPublisher Publisher}'s reserve price.
	 * @return Winning {@link BidInfo}.
	 */
	public BidInfo runAuction(AdxUser user, AdxPublisher publisher,
			double reservePrice) {
		double highetBid = 0;
		BidInfo winningBid = null;
		double secondBid = 0;
		AdxBidBundle winningBundle = null;
		// TODO: consider the max spending limit for a bid/AdN
		for (AdxBidBundle bidBundle : bidBundles) {
			BidInfo bidEntry = bidBundle.getBidEntry(user, publisher,
					ignoreMinorAttributes);
			if (bidEntry.getBid() > highetBid) {
				secondBid = highetBid;
				highetBid = bidEntry.getBid();
				winningBid = bidEntry;
				winningBundle = bidBundle;
			} else if (bidEntry.getBid() > secondBid) {
				secondBid = bidEntry.getBid();
			}
		}
		if (bidBundles.size() == 0) {
			eventBus.post(new AuctionCanceled(user, publisher, secondBid,
					CancellationReason.NO_BIDS));
			return null;
		}
		if (secondBid == 0) {
			secondBid = highetBid;
		}
		// make sure that the publisher will get at least as he requested in the
		// reservePrice parameter
		if (secondBid < reservePrice) {
			eventBus.post(new AuctionCanceled(user, publisher, secondBid,
					CancellationReason.LOW_BIDS));
			return null;
		}
		eventBus.post(new AuctionWon(user, publisher, reservePrice,
				winningBundle.getAdNetwork(), winningBid, secondBid));
		BidInfo clone = (BidInfo) winningBid.clone();
		clone.setBid(secondBid);
		return clone;
	}
}
